package com.swmansion.rnscreens;

import android.content.Context;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;

import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;

import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerModule;
import com.google.android.material.transition.FadeProvider;
import com.google.android.material.transition.MaterialFade;
import com.google.android.material.transition.MaterialFadeThrough;
import com.google.android.material.transition.MaterialSharedAxis;
import com.google.android.material.transition.SlideDistanceProvider;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

public class ScreenStack extends ScreenContainer<ScreenStackFragment> {

    private static final String BACK_STACK_TAG = "RN_SCREEN_LAST";

    private final ArrayList<ScreenStackFragment> mStack = new ArrayList<>();
    private final Set<ScreenStackFragment> mDismissed = new HashSet<>();

    private ScreenStackFragment mTopScreen = null;
    private boolean mRemovalTransitionStarted = false;

    private int screenWidth = -1;

    private final FragmentManager.OnBackStackChangedListener mBackStackListener = new FragmentManager.OnBackStackChangedListener() {
        @Override
        public void onBackStackChanged() {
            if (mFragmentManager.getBackStackEntryCount() == 0) {
                // when back stack entry count hits 0 it means the user's navigated back using hw back
                // button. As the "fake" transaction we installed on the back stack does nothing we need
                // to handle back navigation on our own.
                dismiss(mTopScreen);
            }
        }
    };

    private final FragmentManager.FragmentLifecycleCallbacks mLifecycleCallbacks = new FragmentManager.FragmentLifecycleCallbacks() {
        @Override
        public void onFragmentResumed(FragmentManager fm, Fragment f) {
            if (mTopScreen == f) {
                setupBackHandlerIfNeeded(mTopScreen);
            }
        }
    };

    public ScreenStack(Context context) {
        super(context);
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        int width = dm.widthPixels;// 屏幕宽度（像素）
        float density = dm.density;//屏幕密度（0.75 / 1.0 / 1.5）
        //屏幕宽度算法:屏幕宽度（像素）/屏幕密度
        this.screenWidth = width;//(int) (width/density);
    }

    public void dismiss(ScreenStackFragment screenFragment) {
        mDismissed.add(screenFragment);
        markUpdated();
    }

    public Screen getTopScreen() {
        return mTopScreen != null ? mTopScreen.getScreen() : null;
    }

    public Screen getRootScreen() {
        for (int i = 0, size = getScreenCount(); i < size; i++) {
            Screen screen = getScreenAt(i);
            if (!mDismissed.contains(screen.getFragment())) {
                return screen;
            }
        }
        throw new IllegalStateException("Stack has no root screen set");
    }

    @Override
    protected ScreenStackFragment adapt(Screen screen) {
        return new ScreenStackFragment(screen);
    }

    @Override
    protected void onDetachedFromWindow() {
        if (mFragmentManager != null) {
            mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
            mFragmentManager.unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
            if (!mFragmentManager.isStateSaved()) {
                // state save means that the container where fragment manager was installed has been unmounted.
                // This could happen as a result of dismissing nested stack. In such a case we don't need to
                // reset back stack as it'd result in a crash caused by the fact the fragment manager is no
                // longer attached.
                mFragmentManager.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
            }
        }
        super.onDetachedFromWindow();
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mFragmentManager.registerFragmentLifecycleCallbacks(mLifecycleCallbacks, false);
    }

    @Override
    public void startViewTransition(View view) {
        super.startViewTransition(view);
        mRemovalTransitionStarted = true;
    }

    @Override
    public void endViewTransition(View view) {
        super.endViewTransition(view);
        if (mRemovalTransitionStarted) {
            mRemovalTransitionStarted = false;
            dispatchOnFinishTransitioning();
        }
    }

    public void onViewAppearTransitionEnd() {
        if (!mRemovalTransitionStarted) {
            dispatchOnFinishTransitioning();
        }
    }

    private void dispatchOnFinishTransitioning() {
        ((ReactContext) getContext())
                .getNativeModule(UIManagerModule.class)
                .getEventDispatcher()
                .dispatchEvent(new StackFinishTransitioningEvent(getId()));
    }

    @Override
    protected void removeScreenAt(int index) {
        Screen toBeRemoved = getScreenAt(index);
        mDismissed.remove(toBeRemoved.getFragment());
        super.removeScreenAt(index);
    }

    @Override
    protected void removeAllScreens() {
        mDismissed.clear();
        super.removeAllScreens();
    }

    @Override
    protected boolean hasScreen(ScreenFragment screenFragment) {
        return super.hasScreen(screenFragment) && !mDismissed.contains(screenFragment);
    }

    @Override
    protected void performUpdate() {
        // remove all screens previously on stack
        for (ScreenStackFragment screen : mStack) {
            if (!mScreenFragments.contains(screen) || mDismissed.contains(screen)) {
                getOrCreateTransaction().remove(screen);
            }
        }

        // When going back from a nested stack with a single screen on it, we may hit an edge case
        // when all screens are dismissed and no screen is to be displayed on top. We need to gracefully
        // handle the case of newTop being NULL, which happens in several places below
        ScreenStackFragment newTop = null; // newTop is nullable, see the above comment ^
        ScreenStackFragment belowTop = null; // this is only set if newTop has TRANSPARENT_MODAL presentation mode

        for (int i = mScreenFragments.size() - 1; i >= 0; i--) {
            ScreenStackFragment screen = mScreenFragments.get(i);
            if (!mDismissed.contains(screen)) {
                if (newTop == null) {
                    newTop = screen;
                    if (newTop.getScreen().getStackPresentation() != Screen.StackPresentation.TRANSPARENT_MODAL) {
                        break;
                    }
                } else {
                    belowTop = screen;
                    break;
                }
            }
        }

        for (ScreenStackFragment screen : mScreenFragments) {
            // detach all screens that should not be visible
            if (screen != newTop && screen != belowTop && !mDismissed.contains(screen)) {
                getOrCreateTransaction().remove(screen);
            }
        }
        // attach "below top" screen if set
        if (belowTop != null && !belowTop.isAdded()) {
            final ScreenStackFragment top = newTop;
            getOrCreateTransaction().add(getId(), belowTop).runOnCommit(new Runnable() {
                @Override
                public void run() {
                    top.getScreen().bringToFront();
                }
            });
        }

        if (newTop != null && !newTop.isAdded()) {
            getOrCreateTransaction().add(getId(), newTop);
        }


        if (!mStack.contains(newTop)) {
            // if new top screen wasn't on stack we do "open animation" so long it is not the very first screen on stack
            if (mTopScreen != null && newTop != null) {
                // there was some other screen attached before
                int transition = FragmentTransaction.TRANSIT_FRAGMENT_OPEN;
                switch (newTop.getScreen().getStackAnimation()) {
                    case NONE:
                        transition = FragmentTransaction.TRANSIT_NONE;
                        getOrCreateTransaction().setTransition(transition);
                        break;
                    case FADE:
                        transition = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
                        getOrCreateTransaction().setTransition(transition);
                        break;
                    case PUSH: {
                        MaterialSharedAxis axisTransition = new MaterialSharedAxis(MaterialSharedAxis.X, true);
                        SlideDistanceProvider provider = (SlideDistanceProvider) axisTransition.getPrimaryAnimatorProvider();
                        provider.setSlideDistance(screenWidth);
                        axisTransition.setSecondaryAnimatorProvider(null);
                        newTop.setEnterTransition(axisTransition);
                        mTopScreen.setReturnTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false));
                        mTopScreen.setExitTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, false));
                    }
                }
            }
        } else if (mTopScreen != null && !mTopScreen.equals(newTop)) {
            // otherwise if we are performing top screen change we do "back animation"
            int transition = FragmentTransaction.TRANSIT_FRAGMENT_CLOSE;
            switch (mTopScreen.getScreen().getStackAnimation()) {
                case NONE:
                    transition = FragmentTransaction.TRANSIT_NONE;
                    getOrCreateTransaction().setTransition(transition);
                    break;
                case FADE:
                    transition = FragmentTransaction.TRANSIT_FRAGMENT_FADE;
                    getOrCreateTransaction().setTransition(transition);
                    break;
                case PUSH: {
                    MaterialSharedAxis axisTransition = new MaterialSharedAxis(MaterialSharedAxis.X, false);
                    SlideDistanceProvider provider = (SlideDistanceProvider) axisTransition.getPrimaryAnimatorProvider();
                    provider.setSlideDistance(screenWidth);
                    axisTransition.setSecondaryAnimatorProvider(null);
                    mTopScreen.setExitTransition(axisTransition);
                    mTopScreen.setReturnTransition(axisTransition);
                    newTop.setEnterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true));
                    newTop.setReenterTransition(new MaterialSharedAxis(MaterialSharedAxis.Z, true));
                    newTop.postponeEnterTransition();
                }
            }

        }

        mTopScreen = newTop;

        mStack.clear();
        mStack.addAll(mScreenFragments);

        tryCommitTransaction();

        if (mTopScreen != null) {
            setupBackHandlerIfNeeded(mTopScreen);
        }

        for (ScreenStackFragment screen : mStack) {
            screen.onStackUpdate();
        }
    }

    /**
     * The below method sets up fragment manager's back stack in a way that it'd trigger our back
     * stack change listener when hw back button is clicked.
     * <p>
     * Because back stack by default rolls back the transaction the stack entry is associated with we
     * generate a "fake" transaction that hides and shows the top fragment. As a result when back
     * stack entry is rolled back nothing happens and we are free to handle back navigation on our
     * own in `mBackStackListener`.
     * <p>
     * We pop that "fake" transaction each time we update stack and we add a new one in case the top
     * screen is allowed to be dismised using hw back button. This way in the listener we can tell
     * if back button was pressed based on the count of the items on back stack. We expect 0 items
     * in case hw back is pressed becakse we try to keep the number of items at 1 by always resetting
     * and adding new items. In case we don't add a new item to back stack we remove listener so that
     * it does not get triggered.
     * <p>
     * It is important that we don't install back handler when stack contains a single screen as in
     * that case we want the parent navigator or activity handler to take over.
     */
    private void setupBackHandlerIfNeeded(ScreenStackFragment topScreen) {
        if (!mTopScreen.isResumed()) {
            // if the top fragment is not in a resumed state, adding back stack transaction would throw.
            // In such a case we skip installing back handler and use FragmentLifecycleCallbacks to get
            // notified when it gets resumed so that we can install the handler.
            return;
        }
        mFragmentManager.removeOnBackStackChangedListener(mBackStackListener);
        mFragmentManager.popBackStack(BACK_STACK_TAG, FragmentManager.POP_BACK_STACK_INCLUSIVE);
        ScreenStackFragment firstScreen = null;
        for (int i = 0, size = mStack.size(); i < size; i++) {
            ScreenStackFragment screen = mStack.get(i);
            if (!mDismissed.contains(screen)) {
                firstScreen = screen;
                break;
            }
        }
        if (topScreen != firstScreen && topScreen.isDismissable()) {
            mFragmentManager
                    .beginTransaction()
                    .show(topScreen)
                    .addToBackStack(BACK_STACK_TAG)
                    .setPrimaryNavigationFragment(topScreen)
                    .commitAllowingStateLoss();
            mFragmentManager.addOnBackStackChangedListener(mBackStackListener);
        }
    }
}
